/****************************************************************************
 *
 * Copyright 2016 Samsung Electronics All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 *
 ****************************************************************************/
/****************************************************************************
 * tools/mkdeps.c
 *
 *   Copyright (C) 2012-2013 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <sys/stat.h>

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <limits.h>
#include <ctype.h>
#include <libgen.h>
#include <errno.h>

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define MAX_BUFFER  (4096)

/* NAME_MAX is typically defined in limits.h */

#if !defined(NAME_MAX)

/* FILENAME_MAX might be defined in stdio.h */

#if defined(FILENAME_MAX)
#define NAME_MAX FILENAME_MAX
#else

/* MAXNAMELEN might be defined in dirent.h */

#include <dirent.h>
#if defined(MAXNAMLEN)
#define NAME_MAX MAXNAMLEN
#else

/* Lets not let a silly think like this stop us... just make something up */

#define NAME_MAX 256
#endif
#endif
#endif

/****************************************************************************
 * Private Types
 ****************************************************************************/

enum slashmode_e {
	MODE_FSLASH = 0,
	MODE_BSLASH = 1,
	MODE_DBLBACK = 2
};

/****************************************************************************
 * Private Data
 ****************************************************************************/

static char *g_cc = NULL;
static char *g_cflags = NULL;
static char *g_files = NULL;
static char *g_altpath = NULL;
static char *g_objpath = NULL;
static char *g_suffix = ".o";
static int g_debug = 0;
static bool g_winnative = false;
#ifdef HAVE_WINPATH
static bool g_winpath = false;
static char *g_topdir = NULL;
#define DELIM "\\"
#else
#define DELIM "/"
#endif

static char g_command[MAX_BUFFER];

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/* MinGW does not seem to provide strtok_r */

#ifndef HAVE_STRTOK_R
static char *MY_strtok_r(char *str, const char *delim, char **saveptr)
{
	char *pbegin;
	char *pend = NULL;

	/* Decide if we are starting a new string or continuing from
	 * the point we left off.
	 */

	if (str)
		pbegin = str;
	else if (saveptr && *saveptr)
		pbegin = *saveptr;
	else
		return NULL;

	/* Find the beginning of the next token */

	for (; *pbegin && strchr(delim, *pbegin) != NULL; pbegin++) ;

	/* If we are at the end of the string with nothing
	 * but delimiters found, then return NULL.
	 */

	if (!*pbegin)
		return NULL;

	/* Find the end of the token */

	for (pend = pbegin + 1; *pend && strchr(delim, *pend) == NULL; pend++) ;

	/* pend either points to the end of the string or to
	 * the first delimiter after the string.
	 */

	if (*pend) {
		/* Turn the delimiter into a null terminator */

		*pend++ = '\0';
	}

	/* Save the pointer where we left off and return the
	 * beginning of the token.
	 */

	if (saveptr)
		*saveptr = pend;
	return pbegin;
}

#undef strtok_r
#define strtok_r MY_strtok_r
#endif

static void append(char **base, char *str)
{
	char *oldbase;
	char *newbase;
	int alloclen;

	oldbase = *base;
	if (!oldbase) {
		newbase = strdup(str);
		if (!newbase) {
			fprintf(stderr, "ERROR: Failed to strdup %s\n", str);
			exit(EXIT_FAILURE);
		}
	} else {
		alloclen = strlen(oldbase) + strlen(str) + 2;
		newbase = (char *)malloc(alloclen);
		if (!newbase) {
			fprintf(stderr, "ERROR: Failed to allocate %d bytes\n", alloclen);
			exit(EXIT_FAILURE);
		}

		snprintf(newbase, alloclen, "%s %s\n", oldbase, str);
		free(oldbase);
	}

	*base = newbase;
}

static void show_usage(const char *progname, const char *msg, int exitcode)
{
	if (msg) {
		fprintf(stderr, "\n");
		fprintf(stderr, "%s:\n", msg);
	}

	fprintf(stderr, "\n");
	fprintf(stderr, "%s  [OPTIONS] CC -- CFLAGS -- file [file [file...]]\n", progname);
	fprintf(stderr, "\n");
	fprintf(stderr, "Where:\n");
	fprintf(stderr, "  CC\n");
	fprintf(stderr, "    A variable number of arguments that define how to execute the compiler\n");
	fprintf(stderr, "  CFLAGS\n");
	fprintf(stderr, "    The compiler compilation flags\n");
	fprintf(stderr, "  file\n");
	fprintf(stderr, "    One or more C files whose dependencies will be checked.  Each file is expected\n");
	fprintf(stderr, "    to reside in the current directory unless --dep-path is provided on the command line\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "And [OPTIONS] include:\n");
	fprintf(stderr, "  --dep-debug\n");
	fprintf(stderr, "    Enable script debug\n");
	fprintf(stderr, "  --dep-path <path>\n");
	fprintf(stderr, "    Do not look in the current directory for the file.  Instead, look in <path> to see\n");
	fprintf(stderr, "    if the file resides there.  --dep-path may be used multiple times to specify\n");
	fprintf(stderr, "    multiple alternative location\n");
	fprintf(stderr, "  --obj-path <path>\n");
	fprintf(stderr, "    The final objects will not reside in this path but, rather, at the path provided by\n");
	fprintf(stderr, "    <path>.  if provided multiple time, only the last --obj-path will be used.\n");
	fprintf(stderr, "  --obj-suffix <suffix>\n");
	fprintf(stderr, "    If and object path is provided, then the extension will be assumed to be .o.  This\n");
	fprintf(stderr, "    default suffix can be overrided with this command line option.\n");
	fprintf(stderr, "  --winnative\n");
	fprintf(stderr, "    By default, a POSIX-style environment is assumed (e.g., Linux, Cygwin, etc.)  This option is\n");
	fprintf(stderr, "    inform the tool that is working in a pure Windows native environment.\n");
#ifdef HAVE_WINPATH
	fprintf(stderr, "  --winpaths <TOPDIR>\n");
	fprintf(stderr, "    This option is useful when using a Windows native toolchain in a POSIX environment (such\n");
	fprintf(stderr, "    such as Cygwin).  In this case, will CC generates dependency lists using Windows paths\n");
	fprintf(stderr, "    (e.g., C:\\blablah\\blabla).  This switch instructs the script to use 'cygpath' to convert\n");
	fprintf(stderr, "    the Windows paths to Cygwin POSIXE paths.\n");
#endif
	fprintf(stderr, "  --help\n");
	fprintf(stderr, "    Shows this message and exits\n");
	exit(exitcode);
}

static void parse_args(int argc, char **argv)
{
	char *args = NULL;
	int argidx;

	/* Accumulate CFLAGS up to "--" */

	for (argidx = 1; argidx < argc; argidx++) {
		if (strncmp(argv[argidx], "--", strlen("--") + 1) == 0) {
			g_cc = g_cflags;
			g_cflags = args;
			args = NULL;
		} else if (strncmp(argv[argidx], "--dep-debug", strlen("--dep-debug") + 1) == 0)
			g_debug++;
		else if (strncmp(argv[argidx], "--dep-path", strlen("--dep-path") + 1) == 0) {
			argidx++;
			if (argidx >= argc)
				show_usage(argv[0], "ERROR: Missing argument to --dep-path", EXIT_FAILURE);

			if (args)
				append(&args, argv[argidx]);
			else
				append(&g_altpath, argv[argidx]);
		} else if (strncmp(argv[argidx], "--obj-path", strlen("--obj-path") + 1) == 0) {
			argidx++;
			if (argidx >= argc)
				show_usage(argv[0], "ERROR: Missing argument to --obj-path", EXIT_FAILURE);

			g_objpath = argv[argidx];
		} else if (strncmp(argv[argidx], "--obj-suffix", strlen("--obj-suffix") + 1) == 0) {
			argidx++;
			if (argidx >= argc)
				show_usage(argv[0], "ERROR: Missing argument to --obj-suffix", EXIT_FAILURE);

			g_suffix = argv[argidx];
		} else if (strncmp(argv[argidx], "--winnative", strlen("--winnative") + 1) == 0)
			g_winnative = true;
#ifdef HAVE_WINPATH
		else if (strncmp(argv[argidx], "--winpath", strlen("--winpath") + 1) == 0) {
			g_winpath = true;
			if (g_topdir)
				free(g_topdir);

			argidx++;
			if (argidx >= argc)
				show_usage(argv[0], "ERROR: Missing argument to --winpath", EXIT_FAILURE);

			g_topdir = strdup(argv[argidx]);
		}
#endif
		else if (strncmp(argv[argidx], "--help", strlen("--help") + 1) == 0)
			show_usage(argv[0], NULL, EXIT_SUCCESS);
		else
			append(&args, argv[argidx]);
	}

	/* The final thing accumulated is the list of files */

	g_files = args;

	/* If no paths were specified, then look in the current directory only */

	if (!g_altpath)
		g_altpath = strdup(".");

	if (g_debug) {
		fprintf(stderr, "SELECTIONS\n");
		fprintf(stderr, "  CC             : [%s]\n", g_cc ? g_cc : "(None)");
		fprintf(stderr, "  CFLAGS         : [%s]\n", g_cflags ? g_cflags : "(None)");
		fprintf(stderr, "  FILES          : [%s]\n", g_files ? g_files : "(None)");
		fprintf(stderr, "  PATHS          : [%s]\n", g_altpath ? g_altpath : "(None)");
		if (g_objpath) {
			fprintf(stderr, "  OBJDIR         : [%s]\n", g_objpath);
			fprintf(stderr, "  SUFFIX         : [%s]\n", g_suffix);
		} else
			fprintf(stderr, "  OBJDIR         : (None)\n");

#ifdef HAVE_WINPATH
		fprintf(stderr, "  Windows Paths  : [%s]\n", g_winpath ? "TRUE" : "FALSE");
		if (g_winpath)
			fprintf(stderr, "  TOPDIR         : [%s]\n", g_topdir);
#endif
		fprintf(stderr, "  Windows Native : [%s]\n", g_winnative ? "TRUE" : "FALSE");
	}

	/* Check for required parameters */

	if (!g_cc)
		show_usage(argv[0], "ERROR: No compiler specified", EXIT_FAILURE);

	if (!g_files) {
		/* Don't report an error -- this happens normally in some configurations */

		printf("# No files specified for dependency generataion\n");
		exit(EXIT_SUCCESS);
	}
#ifdef HAVE_WINPATH
	if (g_winnative && g_winpath)
		show_usage(argv[0], "ERROR: Both --winnative and --winpapth makes no sense", EXIT_FAILURE);
#endif
}

static void do_dependency(const char *file, char separator)
{
	static const char moption[] = " -M ";
	struct stat buf;
	char *alloc;
	char *altpath;
	char *path;
	char *lasts;
	int cmdlen;
	int pathlen;
	int filelen;
	int totallen;
	int ret;
	size_t g_cflags_len;

	/* Copy the compiler into the command buffer */

	cmdlen = strlen(g_cc);
	if (cmdlen >= MAX_BUFFER) {
		fprintf(stderr, "ERROR: Compiler string is too long [%d/%d]: %s\n", cmdlen, MAX_BUFFER, g_cc);
		exit(EXIT_FAILURE);
	}

	strncpy(g_command, g_cc, cmdlen + 1);

	/* Copy " -M " */

	cmdlen += strlen(moption);
	if (cmdlen >= MAX_BUFFER) {
		fprintf(stderr, "ERROR: Option string is too long [%d/%d]: %s\n", cmdlen, MAX_BUFFER, moption);
		exit(EXIT_FAILURE);
	}

	/* Copy " -MT " */

	if (g_objpath) {
		char tmp[NAME_MAX + 6];
		char *dupname;
		char *objname;
		char *dotptr;
		size_t tmp_len;

		dupname = strdup(file);
		if (!dupname) {
			fprintf(stderr, "ERROR: Failed to dup: %s\n", file);
			exit(EXIT_FAILURE);
		}

		objname = basename(dupname);
		dotptr = strrchr(objname, '.');
		if (dotptr)
			*dotptr = '\0';

		snprintf(tmp, NAME_MAX + 6, " -MT %s" DELIM "%s%s ", g_objpath, objname, g_suffix);

		tmp_len = strlen(tmp);
		cmdlen += tmp_len;
		if (cmdlen >= MAX_BUFFER) {
			fprintf(stderr, "ERROR: Option string is too long [%d/%d]: %s\n", cmdlen, MAX_BUFFER, moption);
			exit(EXIT_FAILURE);
		}

		strncat(g_command, tmp, tmp_len + 1);
		free(dupname);
	}

	strncat(g_command, moption, strlen(moption) + 1);

	/* Copy the CFLAGS into the command buffer */

	if (g_cflags) {
		g_cflags_len = strlen(g_cflags);
		cmdlen += g_cflags_len;
		if (cmdlen >= MAX_BUFFER) {
			fprintf(stderr, "ERROR: CFLAG string is too long [%d/%d]: %s\n", cmdlen, MAX_BUFFER, g_cflags);
			exit(EXIT_FAILURE);
		}

		strncat(g_command, g_cflags, g_cflags_len + 1);
	}

	/* Add a space */

	g_command[cmdlen] = ' ';
	cmdlen++;
	if (cmdlen >= MAX_BUFFER) {
		fprintf(stderr, "ERROR: CFLAG string is too long [%d/%d]: %s\n", cmdlen, MAX_BUFFER, g_cflags);
		exit(EXIT_FAILURE);
	}

	g_command[cmdlen] = '\0';

	/* Make a copy of g_altpath. We need to do this because at least the version
	 * of strtok_r above does modify it.
	 */

	alloc = strdup(g_altpath);
	if (!alloc) {
		fprintf(stderr, "ERROR: Failed to strdup paths\n");
		exit(EXIT_FAILURE);
	}

	altpath = alloc;

	/* Try each path.  This loop will continue until each path has been tried
	 * (failure) or until stat() finds the file
	 */

	while ((path = strtok_r(altpath, " ", &lasts)) != NULL) {
		/* Create a full path to the file */

		pathlen = strlen(path);
		totallen = cmdlen + pathlen;
		if (totallen >= MAX_BUFFER) {
			fprintf(stderr, "ERROR: Path is too long [%d/%d]: %s\n", totallen, MAX_BUFFER, path);
			exit(EXIT_FAILURE);
		}

		strncpy(&g_command[cmdlen], path, pathlen + 1);

		if (g_command[totallen] != '\0') {
			fprintf(stderr, "ERROR: Missing NUL terminator\n");
			exit(EXIT_FAILURE);
		}

		if (g_command[totallen - 1] != separator) {
			g_command[totallen] = separator;
			pathlen++;
			totallen++;
			if (totallen >= MAX_BUFFER) {
				fprintf(stderr, "ERROR: Path is too long [%d/%d]: %s\n", totallen, MAX_BUFFER, path);
				exit(EXIT_FAILURE);
			}
			g_command[totallen] = '\0';
		}

		filelen = strlen(file);
		totallen += filelen;
		if (totallen >= MAX_BUFFER) {
			fprintf(stderr, "ERROR: Path+file is too long [%d/%d]\n", totallen, MAX_BUFFER);
			exit(EXIT_FAILURE);
		}

		strncat(g_command, file, filelen + 1);

		/* Check that a file actually exists at this path */

		if (g_debug)
			fprintf(stderr, "Trying path=%s file=%s fullpath=%s\n", path, file, &g_command[cmdlen]);

		ret = stat(&g_command[cmdlen], &buf);
		if (ret < 0) {
			altpath = NULL;
			continue;
		}

		if (!S_ISREG(buf.st_mode)) {
			fprintf(stderr, "ERROR: File %s exists but is not a regular file\n", &g_command[cmdlen]);
			exit(EXIT_FAILURE);
		}

		/* Okay.. we have.  Create the dependency.  One a failure to start the
		 * compiler, system() will return -1;  Otherwise, the returned value
		 * from the compiler is in WEXITSTATUS(ret).
		 */

		if (g_debug)
			fprintf(stderr, "Executing: %s\n", g_command);

		ret = system(g_command);
#ifdef WEXITSTATUS
		if (ret < 0 || WEXITSTATUS(ret) != 0) {
			if (ret < 0)
				fprintf(stderr, "ERROR: system failed: %s\n", strerror(errno));
			else
				fprintf(stderr, "ERROR: %s failed: %d\n", g_cc, WEXITSTATUS(ret));

			fprintf(stderr, "       command: %s\n", g_command);
			exit(EXIT_FAILURE);
		}
#else
		if (ret < 0) {
			fprintf(stderr, "ERROR: system failed: %s\n", strerror(errno));
			fprintf(stderr, "       command: %s\n", g_command);
			exit(EXIT_FAILURE);
		}
#endif

		/* We don't really know that the command succeeded... Let's assume that it did */

		free(alloc);
		return;
	}

	printf("# ERROR: File \"%s\" not found at any location\n", file);
	exit(EXIT_FAILURE);
}

/* Convert a Cygwin path to a Windows path */

#ifdef HAVE_WINPATH
static char *cywin2windows(const char *str, const char *append, enum slashmode_e mode)
{
	static const char cygdrive[] = "/cydrive";
	char *dest;
	char *newpath;
	char *allocpath = NULL;
	int srclen = strlen(str);
	int alloclen = 0;
	int drive = 0;
	int lastchar;

	/* Skip any leading whitespace */

	while (isspace(*str))
		str++;

	/* Were we asked to append something? */

	if (append) {
		alloclen = sizeof(str) + sizeof(append) + 1;
		allocpath = (char *)malloc(alloclen);
		if (!allocpath) {
			fprintf(stderr, "ERROR: Failed to allocate %d bytes\n", alloclen);
			exit(EXIT_FAILURE);
		}

		snprintf(allocpath, alloclen, "%s/%s", str, append);
		str = allocpath;
	}

	/* Looking for path of the form /cygdrive/c/bla/bla/bla */

	if (strcasecmp(str, cygdrive) == 0) {
		int cygsize = sizeof(cygdrive);
		if (str[cygsize] == '/') {
			cygsize++;
			srclen -= cygsize;
			str += cygsize;

			if (srclen <= 0) {
				fprintf(stderr, "ERROR: Unhandled path: \"%s\"\n", str);
				exit(EXIT_FAILURE);
			}

			drive = toupper(*str);
			if (drive < 'A' || drive > 'Z') {
				fprintf(stderr, "ERROR: Drive character: \"%s\"\n", str);
				exit(EXIT_FAILURE);
			}

			srclen--;
			str++;
			alloclen = 2;
		}
	}

	/* Determine the size of the new path */

	alloclen += sizeof(str) + 1;
	if (mode == MODE_DBLBACK) {
		const char *tmpptr;
		for (tmpptr = str; *tmpptr; tmpptr++) {
			if (*tmpptr == '/')
				alloclen++;
		}
	}

	/* Allocate memory for the new path */

	newpath = (char *)malloc(alloclen);
	if (!newpath) {
		fprintf(stderr, "ERROR: Failed to allocate %d bytes\n", alloclen);
		exit(EXIT_FAILURE);
	}

	dest = newpath;

	/* Copy the drive character */

	if (drive) {
		*dest++ = drive;
		*dest++ = ':';
	}

	/* Copy each character from the source, making modifications for foward
	 * slashes as required.
	 */

	lastchar = '\0';
	for (; *str; str++) {
		if (mode != MODE_FSLASH && *str == '/') {
			if (lastchar != '/') {
				*dest++ = '\\';
				if (mode == MODE_DBLBACK)
					*dest++ = '\\';
			}
		} else
			*dest++ = *str;

		lastchar = *str;
	}

	*dest++ = '\0';
	if (allocpath)
		free(allocpath);
	return dest;
}
#endif

#ifdef HAVE_WINPATH
static void do_winpath(char *file)
{
	/* The file is in POSIX format.  CC expects Windows format to generate the
	 * dependencies, but GNU make expect the resulting dependencies to be back
	 * in POSIX format.  What a mess!
	 */

	char *path = cywin2windows(g_topdir, file, MODE_FSLASH);

	/* Then get the dependency and perform conversions on it to make it
	 * palatable to the Cygwin make.
	 */
#warning "Missing logic"

	free(path);
}
#endif

/****************************************************************************
 * Public Functions
 ****************************************************************************/

int main(int argc, char **argv, char **envp)
{
	char *lasts;
	char *files;
	char *file;

	/* Parse command line parameters */

	parse_args(argc, argv);

	/* Then generate dependencies for each path on the command line.  NOTE
	 * strtok_r will clobber the files list.  But that is okay because we are
	 * only going to traverse it once.
	 */

	files = g_files;
	while ((file = strtok_r(files, " ", &lasts)) != NULL) {
		/* Check if we need to do path conversions for a Windows-natvive tool
		 * being using in a POSIX/Cygwin environment.
		 */

#ifdef HAVE_WINPATH
		if (g_winpath)
			do_winpath(file);
		else
#endif
		{
			do_dependency(file, g_winnative ? '\\' : '/');
		}

		files = NULL;
	}

	return EXIT_SUCCESS;
}
